#include <app.hpp>

#include <random>
#include <ctime>

/*                  0  1  2  3  4  5  6  7  8  mine
  mine opened    : 10 11 12 13 14 15 16 17 18 19
  mine not opened: 20 21 22 23 24 25 26 27 28 29
  mine flag      : 30 31 32 33 34 35 36 37 38 39
  mine what      : 40 41 42 43 44 45 46 47 48 49
*/

#define MINE_ID_CHECK_GAMEOVER (APP_USER_EVENT_ID+1)

enum class eState : cvs::uint8 {
  Start,Runing,Finish,Win
};

class MineSweeper : public App {
  protected:
    struct Point { int x; int y; };
    struct Size { int w; int h; };
  public:
    MineSweeper() : maps_(0) {
      state_ = eState::Start;
      map_size_.w = 10;
      map_size_.h = 10;
      item_size_.w = 50;
      item_size_.h = 50;
      mine_count_ = 10;
      map_gened_ = false;
      level_ = 1;

      ctrl_play_ = 0;
    }
    virtual ~MineSweeper() {}
  protected:
    virtual cvs::Result create_resource() override {
      auto rst = render_->create_shape_rect(cvs::Rect(), cvs::Size(), &shape_rect_);
      music_link_ok_ = load_music("sound/ok.mp3");
      music_link_err_ = load_music("sound/warning.mp3");
      return rst;
    }
    virtual void release_resource() override {
      cvs::SaveDecref(shape_rect_); shape_rect_ = 0;
      unload_music(music_link_err_);
      unload_music(music_link_ok_);
      free_map();
    }
    virtual cvs::Result create_target_resource() override {
      auto rst = target_->create_solid_color_brush(cvs::Color(), &brush_);
      if (cvs::Succeeded(rst))
        rst = load_ctrls();
      return rst;
    }
    virtual void release_target_resource() override {
      unload_ctrls();
      cvs::SaveDecref(brush_); brush_ = 0;
    }
    cvs::Result load_ctrls() {
      auto size = window_size();
      cvs::dtype w = 100.f;
      cvs::dtype h = 50.f;
      cvs::dtype x = size.w / 2.f - w / 2.f;
      cvs::dtype y = size.h / 2.f - h / 2.f;
      cvs::Rect rc(x, y, w, h);

      auto ctrl_play = new AppControl(this);
      if (ctrl_play) {
        ctrl_play->set_rect(rc);
        ctrl_play->set_radius(cvs::Size(5.f, 5.f));
        ctrl_play->set_use_back_color(true);
        ctrl_play->set_text(L"Play");
        ctrl_play->normal.back_color = cvs::Color(1.f,0.66f,0.14f);
        ctrl_play->normal.text_color = cvs::Color(1.f,1.f,0.96f,0.9f);
        ctrl_play->normal.font_format.size = 20.f;
        ctrl_play->normal.text_format.halign = cvs::c2d::TextAlignHor::Center;
        ctrl_play->normal.text_format.valign = cvs::c2d::TextAlignVer::Middle;
        ctrl_play->hot.back_color = cvs::Color(1.f,0.59f,0.f);
        ctrl_play->hot.border_color = cvs::Color(0.f,1.f,0.f);
        ctrl_play->hot.text_color = cvs::Color(1.f,1.f,0.96f,0.9f);
        ctrl_play->hot.font_format.size = 20.f;
        ctrl_play->hot.text_format.halign = cvs::c2d::TextAlignHor::Center;
        ctrl_play->hot.text_format.valign = cvs::c2d::TextAlignVer::Middle;
        ctrl_play->pushed.back_color = cvs::Color(95.f,0.5f,0.02f);
        ctrl_play->pushed.text_color = cvs::Color(1.f,1.f,0.96f,0.9f);
        ctrl_play->pushed.font_format.size = 20.f;
        ctrl_play->pushed.text_format.halign = cvs::c2d::TextAlignHor::Center;
        ctrl_play->pushed.text_format.valign = cvs::c2d::TextAlignVer::Middle;
        ctrl_play->set_callback(std::bind(&MineSweeper::on_ctrl_play, this, std::placeholders::_1));
        ctrl_play_ = ctrl_play;
      }
      return CVS_S_Ok;
    }
    void unload_ctrls() {
      if (ctrl_play_) {
        delete ctrl_play_;
        ctrl_play_ = 0;
      }
    }
  protected:
    virtual void on_draw() override {
      cvs::Color bkcolor(0.7f, 0.7f, 0.7f);
      target_->clear(&bkcolor);
      switch (state_) {
        case eState::Start:
          draw_start();
          break;
        case eState::Runing:
          draw_runing();
          break;
        case eState::Win:
          draw_win();
          break;
        case eState::Finish:
          draw_finish();
          break;
      }
      draw_level();
    }
    virtual void on_timer(SDL_TimerID id, Uint32 interval, void *param) override {
      if (id == timer_time_) {
        time_end_ = SDL_GetTicks();
        invaldate();
      } else if (id == timer_toplay_) {
        remove_timer(timer_toplay_);
        timer_toplay_ = 0;
        if (state_ == eState::Finish) {
          state_ = eState::Start;
          ctrl_play_->set_visible(true);
        } else if (state_ == eState::Win) {
          state_ = eState::Runing;
          time_begin_ = SDL_GetTicks();
          time_end_ = SDL_GetTicks();
          ++level_;
          new_level();
        }
        invaldate();
      }
    }
    virtual void on_user(Sint32 id, void *data1, void *data2) override {
      if (id == MINE_ID_CHECK_GAMEOVER) {
        auto g = gameover();
        if (g < 0) {
          state_ = eState::Finish;
          invaldate();
          remove_timer(timer_time_);
          timer_time_ = 0;
          timer_toplay_ = add_timer(3000);
        } else if (g == 0) {
          state_ = eState::Win;
          invaldate();
          timer_toplay_ = add_timer(3000);
        }
      }
    }
    virtual void on_mouse_click(const SDL_MouseButtonEvent &evt) override {
      if (evt.state == SDL_PRESSED && evt.button == SDL_BUTTON_LEFT) {
        if (state_ == eState::Runing) {
          Point pt;
          if (pt_item(evt.x, evt.y, pt)) {
            action(pt.x, pt.y, true);
          }
        }
      } else if (evt.state == SDL_PRESSED && evt.button == SDL_BUTTON_RIGHT) {
        if (state_ == eState::Runing) {
          Point pt;
          if (pt_item(evt.x, evt.y, pt)) {
            action(pt.x, pt.y, false);
          }
        }
      }
    }
  protected:
    void action(int x, int y, bool open) {
      if (!map_gened_) {
        Point pt;
        pt.x = x; pt.y = y;
        gen_map(pt);
      }

      auto d = maps_[y][x];

      if (!open) {
        if (is_opened(d)) return;
        if (is_flag(d)) d += 10;
        else if (is_what(d)) d -= 20;
        else if (is_not_opened(d)) d += 10;
        maps_[y][x] = d;
      } else {
        if (is_opened(d)) {
          auto c = d_count(d);
          if (c>0 && c<9) {
            int imin = x - 1 < 0 ? 0 : x - 1;
            int jmin = y - 1 < 0 ? 0 : y - 1;
            int imax = x + 1 > map_size_.w-1 ? map_size_.w-1 : x + 1;
            int jmax = y + 1 > map_size_.h-1 ? map_size_.h-1 : y + 1;

            int count = 0;
            int mine = 0;
            for (int j = jmin; j <= jmax; ++j) {
              for (int i = imin; i <= imax; ++i) {
                if (is_flag(maps_[j][i]))
                  ++count;
                if (is_mine(maps_[j][i]))
                  ++mine;
              }
            }

            if (count == mine) {
              for (int j = jmin; j <= jmax; ++j) {
                for (int i = imin; i <= imax; ++i) {
                  if (is_not_opened(maps_[j][i]))
                    action(i, j, true);
                }
              }
            } else {
              play_sound(music_link_err_);
            }
          }
          return;
        }
        if (is_flag(d)) return;
        if (is_what(d)) return;

        maps_[y][x] = d - 10;

        if (is_empty(d)) {
          int imin = x - 1 < 0 ? 0 : x - 1;
          int jmin = y - 1 < 0 ? 0 : y - 1;
          int imax = x + 1 > map_size_.w-1 ? map_size_.w-1 : x + 1;
          int jmax = y + 1 > map_size_.h-1 ? map_size_.h-1 : y + 1;

          for (int j = jmin; j <= jmax; ++j) {
            for (int i = imin; i <= imax; ++i) {
              action(i, j, true);
            }
          }
        }
        
        play_sound(music_link_ok_);
      }

      do_user_event(MINE_ID_CHECK_GAMEOVER);
      invaldate();
    }
  protected:
    void draw_start() {}
    void draw_runing() {
      auto size = window_size();
      cvs::dtype x=0.f, y=0.f;
      x = (size.w-map_size_.w*item_size_.w)/2.f;
      y = (size.h-map_size_.h*item_size_.h)/2.f;

      for (int j = 0; j < map_size_.h; ++j) {
        for (int i = 0; i < map_size_.w; ++i) {
          draw_one(x + i * item_size_.w
              , y + j * item_size_.h
              , maps_[j][i]);
        }
      }

      draw_time();
    }
    void draw_finish() {
      draw_runing();
      draw_text(L"Game Over!", window_rect(), 40.f);
      draw_time();
    }
    void draw_win() {
      draw_runing();
      draw_text(L"Win! Next!", window_rect(), 40.f);
      draw_time();
    }
    void draw_one(cvs::dtype x, cvs::dtype y, int d) {
      cvs::Rect rc(x, y, item_size_.w, item_size_.h);
      shape_rect_->set(rc, cvs::Size(5.f, 5.f));
      cvs::dtype font_size = 20.f;

      if (is_opened(d)) {
        if (is_empty(d)) {
        } else if (is_mine(d)) {
          brush_->set_color(cvs::Color(1.f,1.f,0.f));
          target_->fill_shape(shape_rect_, brush_);
          draw_text(L"*", rc, font_size);
        } else {
          auto c = d_count(d);
          draw_text(std::to_wstring(c), rc, font_size);
        }
      } else {
        brush_->set_color(cvs::Color(1.f,1.f,0.f));
        target_->fill_shape(shape_rect_, brush_);
        if (is_flag(d)) {
          draw_text(L"*", rc, font_size);
        } else if (is_what(d)) {
          draw_text(L"?", rc, font_size);
        } else {
        }
      }

      brush_->set_color(cvs::Color(0.f,1.f,0.f));
      target_->draw_shape(shape_rect_, brush_);
    }
    void draw_time() {
      auto time = time_end_ - time_begin_;

      std::wstring text;
      text = L"Time: ";
      text += std::to_wstring(time/1000);

      auto rc = window_rect();

      cvs::c2d::FontFormat font;
      cvs::c2d::TextFormat format;

      font.family = L"";
      font.size = 20.f;
      font.bold = false;
      font.italic = false;
      font.underline = false;
      font.strikethrough = false;
      format.halign = cvs::c2d::TextAlignHor::Right;
      format.valign = cvs::c2d::TextAlignVer::Middle;
      format.direction = cvs::c2d::TextDirection::LeftToRight;
      format.trimming = cvs::c2d::TextTrimming::None;
      format.word_wrap = false;
      format.auto_width = false;
      format.auto_height = true;

      draw_text(text, rc, cvs::Color(1.f, 0.f, 0.f), font, format);
    }
    void draw_level() {
      auto rc = window_rect();

      std::wstring text;
      text = L"Level: ";
      text += std::to_wstring(level_);
      text += L"  Mine :";
      text += std::to_wstring(flag_count());
      text += L"/";
      text += std::to_wstring(mine_count_);

      cvs::c2d::FontFormat font;
      cvs::c2d::TextFormat format;

      font.family = L"";
      font.size = 20.f;
      font.bold = false;
      font.italic = false;
      font.underline = false;
      font.strikethrough = false;
      format.halign = cvs::c2d::TextAlignHor::Left;
      format.valign = cvs::c2d::TextAlignVer::Middle;
      format.direction = cvs::c2d::TextDirection::LeftToRight;
      format.trimming = cvs::c2d::TextTrimming::None;
      format.word_wrap = false;
      format.auto_width = false;
      format.auto_height = true;

      draw_text(text, rc, cvs::Color(1.f, 0.f, 0.f), font, format);
    }
  protected:
    void new_level() {
      item_size_.w = 40;
      item_size_.h = 40;
      map_size_.w = 18;
      map_size_.h = 13;
      mine_count_ = level_ * 5;

      free_map();
      alloc_map();
    }
    bool is_alloc_map() const {
      if (map_size_.h <= 0 || map_size_.w <= 0)
        return false;
      if (!maps_) return false;
      return true;
    }
    void alloc_map() {
      if (map_size_.h <= 0 || map_size_.w <= 0)
        return;

      if (is_alloc_map()) {
        for (int j = 0; j < map_size_.h; ++j) {
          for (int i = 0; i < map_size_.w; ++i)
            maps_[j][i] = 20;
        }
      } else {
        maps_ = new int*[map_size_.h];
        for (int j = 0; j < map_size_.h; ++j) {
          maps_[j] = new int[map_size_.w];
          for (int i = 0; i < map_size_.w; ++i)
            maps_[j][i] = 20;
        }
      }
      map_gened_ = false;
    }
    void free_map() {
      if (!is_alloc_map()) return;
      for (int j = 0; j < map_size_.h; ++j) {
        if (maps_[j]) {
          delete[] maps_[j];
          maps_[j] = 0;
        }
      }
      delete[] maps_;
      maps_ = 0;
    }
    void gen_map(Point pt) {
      alloc_map();
      if (!is_alloc_map()) return;

      std::vector<Point> pts;
      auto fun_in_pt = [&pts](int i, int j) -> bool {
        for (auto &p : pts)
          if (p.x == i && p.y == j) return true;
        return false;
      };
      {
        int imin = pt.x - 1 < 0 ? 0 : pt.x - 1;
        int jmin = pt.y - 1 < 0 ? 0 : pt.y - 1;
        int imax = pt.x + 1 > map_size_.w-1 ? map_size_.w-1 : pt.x + 1;
        int jmax = pt.y + 1 > map_size_.h-1 ? map_size_.h-1 : pt.y + 1;

        for (int j = jmin; j <= jmax; ++j) {
          for (int i = imin; i <= imax; ++i) {
            Point t;
            t.x = i; t.y = j;
            pts.push_back(std::move(t));
          }
        }
      }

      // set to zero
      for (int j = 0; j < map_size_.h; ++j) {
        for (int i = 0; i < map_size_.w; ++i) {
          maps_[j][i] = 0;
        }
      }

      // genarate mine
      for (int mine = 0; mine < mine_count_; ++mine) {
        auto id = random(0, map_size_.w * map_size_.h - 10 - mine);
        bool added = false;
        int k = 0;
        for (int j = 0; j < map_size_.h; ++j) {
          for (int i = 0; i < map_size_.w; ++i) {
            if (is_mine(maps_[j][i])) continue;
            if (fun_in_pt(i, j)) continue;
            if (k == id) {
              maps_[j][i] = 29;
              added = true;
              break;
            }
            ++k;
          }
          if (added) break;
        }
      }

      // calcute others
      for (int j = 0; j < map_size_.h; ++j) {
        for (int i = 0; i < map_size_.w; ++i) {
          if (!is_mine(maps_[j][i])) {
            maps_[j][i] = around_count(i, j) + 20;
          }
        }
      }

      map_gened_ = true;
    }
    bool is_data(int d) const { return (d>=10 && d<= 49); }
    bool is_opened(int d) const { return (d>=10 && d<=19); }
    bool is_not_opened(int d) const { return (d>=20 && d<=29); }
    bool is_flag(int d) const { return (d>=30 && d<=39); }
    bool is_what(int d) const { return (d>=40 && d<=49); }
    bool is_mine(int d) const { return (d_count(d) == 9); }
    bool is_empty(int d) const { return (d_count(d) == 0); }
    int d_count(int d) const { return (d%10); }
    int d_count(int x, int y) const { return d_count(maps_[y][x]); }
    int around_count(int x, int y) const {
      int imin = x - 1 < 0 ? 0 : x - 1;
      int jmin = y - 1 < 0 ? 0 : y - 1;
      int imax = x + 1 > map_size_.w-1 ? map_size_.w-1 : x + 1;
      int jmax = y + 1 > map_size_.h-1 ? map_size_.h-1 : y + 1;

      int count = 0;
      for (int j = jmin; j <= jmax; ++j) {
        for (int i = imin; i <= imax; ++i) {
          if (is_mine(maps_[j][i]))
            ++count;
        }
      }
      return count;
    }
    int flag_count() const {
      if (!is_alloc_map()) return 0;
      int count = 0;
      for (int j = 0; j < map_size_.h; ++j) {
        for (int i = 0; i < map_size_.w; ++i) {
          if (is_flag(maps_[j][i])) ++count;
        }
      }
      return count;
    }
    bool pt_item(int evtx, int evty, Point &pt) {
      auto size = window_size();
      cvs::dtype x=0.f, y=0.f;
      x = (size.w-map_size_.w*item_size_.w)/2.f;
      y = (size.h-map_size_.h*item_size_.h)/2.f;
      if (evtx < x || evty < y) return false;
      int i = (evtx - x) / item_size_.w;
      int j = (evty - y) / item_size_.h;
      if (i >= map_size_.w || j >= map_size_.h) return false;
      pt.x = i; pt.y = j;
      return true;
    }
    int gameover() const {
      int mine_found = 0;
      int not_opened = 0;
      for (int j = 0; j < map_size_.h; ++j) {
        for (int i = 0; i < map_size_.w; ++i) {
          auto d = maps_[j][i];
          if (is_opened(d)) {
            if (is_mine(d)) return -1;
          } else {
            ++not_opened;
            if (is_flag(d))
              ++mine_found;
          }
        }
      }
      return not_opened - mine_found;
    }
  protected:
    void play() {
      new_level();
      state_ = eState::Runing;
      time_begin_ = SDL_GetTicks();
      time_end_ = SDL_GetTicks();
      timer_time_ = add_timer(1000);
      ctrl_play_->set_visible(false);
      
      invaldate();
    }
    void on_ctrl_play(AppControl *ctrl) {
      play();
    }
  protected:
    eState state_;
    int **maps_;
    Size map_size_;
    Size item_size_;
    int mine_count_;
    bool map_gened_;
    int level_;

    cvs::c2d::IShapeRect *shape_rect_;
    cvs::c2d::ISolidColorBrush *brush_;

    SDL_TimerID timer_time_;
    SDL_TimerID timer_toplay_;
    Uint32 time_begin_;
    Uint32 time_end_;
    
    Mix_Music *music_link_ok_;
    Mix_Music *music_link_err_;

    AppControl *ctrl_play_;
};

int main(int argc, char *argv[]) {
  MineSweeper app;

  auto rst = app.init();
  if (cvs::Succeeded(rst))
    rst = app.create_window("Mine Sweeper", cvs::Size(800.f, 600.f), SDL_WINDOW_SHOWN);

  if (cvs::Succeeded(rst))
    app.run(APP_RUN_SLOWEST);

  app.uninit();
  return 0;
}